package bullgo

import (
	"encoding/json"
	"errors"
	"reflect"
	"strconv"
	"strings"
	"time"
)

// Job ...
type Job struct {
	ID           string
	Name         string
	LockKey      string
	Data         map[string]interface{}
	Opts         AddJobOpt
	Timestamp    int64
	Delay        int64
	AttemptsMade int
	Queue        *Queue
	progress     int
	FinishedOn   int64
	ProcessedOn  int64
	FailedReason string
	ReturnValue  string
}

// AddJobOpt ...
type AddJobOpt struct {
	CustomJobID      string `json:"customJobId,omitempty"`
	Priority         int64  `json:"priority,omitempty"`
	Lifo             bool   `json:"lifo,omitempty"`
	Delay            int64  `json:"delay,omitempty"`
	Timestamp        int64  `json:"timestamp,omitempty"`
	Attempts         int64  `json:"attempts,omitempty"`
	RemoveOnFail     bool   `json:"removeOnFail,omitempty"`
	RemoveOnComplete bool   `json:"removeOnComplete,omitempty"`
}

// AddJobOption ...
type AddJobOption struct {
	Name      string
	AddJobOpt AddJobOpt
}

func (job *Job) createWithOption(queue *Queue, data map[string]interface{}, opts AddJobOption) (*string, error) {
	if opts.Name == "" {
		job.Name = "__default__"
	}
	job.Queue = queue
	job.Data = data
	if opts.AddJobOpt.Timestamp == 0 {
		opts.AddJobOpt.Timestamp = time.Now().UnixNano() / 1e6
	}
	if opts.AddJobOpt.Attempts == 0 {
		opts.AddJobOpt.Attempts = 1
	}
	job.Timestamp = opts.AddJobOpt.Timestamp
	job.Delay = opts.AddJobOpt.Delay
	job.Opts = opts.AddJobOpt
	job.AttemptsMade = 0
	return addJob(*job, opts.AddJobOpt)
}

func (job *Job) Create(queue *Queue, data map[string]interface{}) (*string, error) {
	return job.createWithOption(queue, data, AddJobOption{})
}

func newJobFromID(queue *Queue, jobID string) (*Job, error) {
	if jobID == "" {
		return nil, errors.New("have not jobID")
	}
	jobMap, err := queue.HGetAll(queue.Ctx, queue.Tokey(jobID)).Result()
	if err != nil {
		return nil, err
	}
	jobMap["jobID"] = jobID
	return newJobFromMap(queue, jobMap)
}

func newJobFromMap(queue *Queue, jobMap map[string]string) (*Job, error) {
	job := new(Job)
	if len(jobMap) == 0 {
		return nil, errors.New("have not data")
	}
	var jobData map[string]interface{}
	json.Unmarshal([]byte(jobMap["data"]), &jobData)
	job.Data = jobData
	job.Queue = queue
	var opts AddJobOpt
	json.Unmarshal([]byte(jobMap["opts"]), &opts)
	job.Opts = opts
	job.Name = jobMap["name"]
	job.ID = jobMap["jobID"]
	job.LockKey = queue.Tokey(job.ID) + ":lock"
	delayStr, ok := jobMap["delay"]
	if ok {
		job.Delay, _ = strconv.ParseInt(delayStr, 10, 64)
	}
	timestampStr, ok := jobMap["timestamp"]
	if ok {
		job.Timestamp, _ = strconv.ParseInt(timestampStr, 10, 64)
	}
	progressStr, ok := jobMap["progress"]
	if ok {
		job.progress, _ = strconv.Atoi(progressStr)
	}
	finishedOnStr, ok := jobMap["finishedOn"]
	if ok {
		job.FinishedOn, _ = strconv.ParseInt(finishedOnStr, 10, 64)
	}
	processedOnStr, ok := jobMap["processedOn"]
	if ok {
		job.ProcessedOn, _ = strconv.ParseInt(processedOnStr, 10, 64)
	}
	failedReason, ok := jobMap["failedReason"]
	if ok {
		job.FailedReason = failedReason
	}
	returnvalue, ok := jobMap["returnvalue"]
	if ok {
		job.ReturnValue = returnvalue
	}
	return job, nil
}

// Progress ...
func (job *Job) Progress(progress *int) int {
	if progress != nil {
		job.progress = *progress
		_, err := updateProgress(*job, *progress).Result()
		if err != nil {
			return job.progress
		}
		return *progress
	}
	return job.progress
}

// IsDone ...
func (job *Job) IsDone(list string) bool {
	val, _ := job.Queue.ZScore(job.Queue.Ctx, job.Queue.Tokey(list), job.ID).Result()
	return val != 0
}

// IsInList ...
func (job *Job) IsInList(list string) bool {
	queue := job.Queue
	return isJobInList(queue.ExtendRedis, queue.Tokey(list), job.ID)
}

// Remove ...
func (job *Job) Remove() bool {
	queue := job.Queue
	val, _ := remove(queue, job.ID).Result()
	return val.(int64) == 1
}

// Update ...
func (job *Job) Update(data map[string]interface{}) bool {
	dataByte, err := json.Marshal(job.Data)
	if err != nil {
		return false
	}
	queue := job.Queue
	_, err = queue.ExtendRedis.HSet(queue.Ctx, queue.Tokey(job.ID), "data", string(dataByte)).Result()
	if err != nil {
		return false
	}
	return false
}

// MoveToDelayed ...
func (job *Job) MoveToDelayed(timestamp int64, ignoreLock bool) error {
	err := moveToDelayed(job.Queue, job.ID, timestamp, ignoreLock)
	if err != nil {
		return err
	}
	return nil
}

// Promote ...
func (job *Job) Promote() error {
	queue := job.Queue
	jobID := job.ID

	script := strings.Join(
		[]string{
			"if redis.call(\"ZREM\", KEYS[1], ARGV[1]) == 1 then",
			" redis.call(\"LPUSH\", KEYS[2], ARGV[1])",
			" return 0",
			"else",
			" return -1",
			"end",
		},
		"\n",
	)

	keys := []string{Delayed, Wait}
	val, _ := queue.ExtendRedis.ExecScript("Promote", script, keys, jobID).Result()
	if val.(int64) == -1 {
		return errors.New("Job " + jobID + " is not in a delayed state")
	}
	return nil
}

// Retry ...
func (job *Job) Retry() error {
	err := reprocessJob(job, "failed")
	if err != nil {
		return err
	}
	return nil
}

// IsCompleted ...
func (job *Job) IsCompleted() bool {
	return job.IsDone("completed")
}

// IsFailed ...
func (job *Job) IsFailed() bool {
	return job.IsDone("failed")
}

// IsDelayed ...
func (job *Job) IsDelayed() bool {
	return job.IsDone("delayed")
}

// IsActive ...
func (job *Job) IsActive() bool {
	return job.IsInList("active")
}

// IsWaiting ...
func (job *Job) IsWaiting() bool {
	return job.IsInList("wait")
}

// IsPaused ...
func (job *Job) IsPaused() bool {
	return job.IsInList("paused")
}

// IsStuck ...
func (job *Job) IsStuck() bool {
	state := job.GetState()
	return state == "stuck"
}

// GetState ...
func (job *Job) GetState() string {
	ele := reflect.ValueOf(job)
	funcs := []struct {
		fn    string
		state string
	}{
		{"IsCompleted", "completed"},
		{"IsFailed", "failed"},
		{"IsDelayed", "delayed"},
		{"IsActive", "active"},
		{"IsWaiting", "waiting"},
		{"IsPaused", "paused"},
	}
	for _, fn := range funcs {
		result := ele.MethodByName(fn.fn).Call([]reflect.Value{})
		if result[0].Bool() {
			return fn.state
		}
	}
	return "stuck"
}

// MoveToCompleted ...
func (job *Job) MoveToCompleted(returnValue interface{}, ignoreLock bool) error {
	returnValuesByte, err := json.Marshal(returnValue)
	if err != nil {
		return err
	}
	return moveToCompleted(*job, string(returnValuesByte), job.Opts.RemoveOnComplete, false)
}

// MoveToFailed ...
func (job *Job) MoveToFailed(err error, ignoreLock bool) error {
	return moveToFailed(*job, err.Error(), job.Opts.RemoveOnFail, false)
}

// TakeLock ...
func (job *Job) TakeLock() bool {
	val, _ := takeLock(job.Queue, job.ID).Result()
	result := val.(int64)
	return result == 1
}

// ReleaseLock ...
func (job *Job) ReleaseLock() bool {
	val, _ := releaseLock(job.Queue, job.ID).Result()
	result := val.(int64)
	return result == 0
}
